﻿// *********************************************************
// 
//     Copyright (c) Microsoft. All rights reserved.
//     This code is licensed under the Apache License, Version 2.0.
//     THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//     ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//     IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//     PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
// 
// *********************************************************
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using Bio.Algorithms.Alignment;
using Bio.Algorithms.Assembly.Padena.Scaffold;
using System;
using Bio.Util;

namespace Bio.Algorithms.Assembly.Comparative
{
    /// <summary>
    /// Implements a comparative genome assembly for
    /// assembly of DNA sequences.
    /// </summary>
    public class ComparativeGenomeAssembler
    {
        /// <summary>
        /// Holds time interval between two progress events.
        /// </summary>
        private const int ProgressTimerInterval = 5 * 60 * 1000;

        /// <summary>
        /// kmer Length.
        /// </summary>
        private int kmerLength = 10;

        /// <summary>
        /// Length of MUM. 
        /// </summary>
        private int lengthOfMum = 20;

        /// <summary>
        /// Default depth for graph traversal in scaffold builder step.
        /// </summary>
        private int depth = 10;

        /// <summary>
        /// Break Length.
        /// </summary>
        private int breakLength = 1;

        /// <summary>
        /// Holds the status message which will be sent through the Status event.
        /// </summary>
        private string statusMessage;

        /// <summary>
        /// Timer to report progress.
        /// </summary>
        private Timer progressTimer;

        #region Properties
        /// <summary>
        /// Provides the status to the subscribers.
        /// </summary>
        public event EventHandler<StatusChangedEventArgs> StatusChanged;

        /// <summary>
        /// Gets or sets the kmer length.
        /// </summary>
        public int KmerLength
        {
            get
            {
                return this.kmerLength;
            }

            set
            {
                this.kmerLength = value;
                this.AllowKmerLengthEstimation = false;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to estimate kmer length.
        /// </summary>
        public bool AllowKmerLengthEstimation { get; set; }

        /// <summary>
        /// Gets or sets value of redundancy for building scaffolds.
        /// </summary>
        public int ScaffoldRedundancy { get; set; }

        /// <summary>
        /// Gets or sets whether to run scaffolding step or not.
        /// </summary>
        public bool ScaffoldingEnabled { get; set; }

        /// <summary>
        /// Gets or sets the Depth for graph traversal in scaffold builder step.
        /// </summary>
        public int Depth
        {
            get
            {
                return depth;
            }
            set
            {
                depth = value;
            }
        }

        /// <summary>
        /// Gets the name of the sequence assembly algorithm being
        /// implemented. This is intended to give the
        /// developer some information of the current sequence assembly algorithm.
        /// </summary>
        public string Name
        {
            get { return Properties.Resources.PcaName; }
        }

        /// <summary>
        /// Gets the description of the sequence assembly algorithm being
        /// implemented. This is intended to give the
        /// developer some information of the current sequence assembly algorithm.
        /// </summary>
        public string Description
        {
            get { return Properties.Resources.PcaDescription; }
        }

        /// <summary>
        /// Gets or sets the length of MUM for using with NUCmer.
        /// </summary>
        public int LengthOfMum
        {
            get
            {
                return lengthOfMum;
            }

            set
            {
                lengthOfMum = value;
            }
        }

        /// <summary>
        /// Gets or sets number of bases to be extended before stopping alignment in NUCmer
        /// </summary>
        private int BreakLength
        {
            get
            {
                return breakLength;
            }

            set
            {
                breakLength = value;
            }
        }

        #endregion

        /// <summary>
        /// Assemble the input sequences into the largest possible contigs. 
        /// </summary>
        /// <param name="referenceSequence">The sequence used as backbone for assembly.</param>
        /// <param name="reads">The sequences to assemble.</param>
        /// <returns>IComparativeAssembly instance which contains list of assembled sequences.</returns>
        public IEnumerable<ISequence> Assemble(IEnumerable<ISequence> referenceSequence, IEnumerable<ISequence> reads)
        {
            this.progressTimer = new Timer(ProgressTimerInterval);
            this.progressTimer.Elapsed += new ElapsedEventHandler(this.ProgressTimerElapsed);

            // Converting to list to avoid multiple parse of the reference file if its a yield return
            referenceSequence = referenceSequence.ToList();

            //Comparative Assembly Steps
            //1) Read Alignment (Calling NUCmer for aligning reads to reference sequence)
            StatusEventStart(Properties.Resources.ReadAlignmentStarted);
            IList<IEnumerable<DeltaAlignment>> alignmentBetweenReferenceAndReads = this.ReadAlignment(referenceSequence, reads.Where( a => a.Count >= LengthOfMum));
            StatusEventEnd(Properties.Resources.ReadAlignmentEnded);

            // 2) Repeat Resolution
            StatusEventStart(Properties.Resources.RepeatResolutionStarted);
            IEnumerable<DeltaAlignment> repeatResolvedDeltas = RepeatResolution(alignmentBetweenReferenceAndReads);
            StatusEventEnd(Properties.Resources.RepeatResolutionEnded);

            StatusEventStart(Properties.Resources.SortingResolvedDeltasStarted);
            IList<DeltaAlignment> orderedRepeatResolvedDeltas = repeatResolvedDeltas.OrderBy(a => a.FirstSequenceStart).ToList();
            StatusEventEnd(Properties.Resources.SortingResolvedDeltasEnded);

            // 3) Layout Refinement
            StatusEventStart(Properties.Resources.LayoutRefinementStarted);
            LayoutRefinment(orderedRepeatResolvedDeltas);
            StatusEventEnd(Properties.Resources.LayoutRefinementEnded);

            // 4) Consensus Generation
            StatusEventStart(Properties.Resources.ConsensusGenerationStarted);
            IEnumerable<ISequence> contigs = this.ConsensusGenerator(orderedRepeatResolvedDeltas.OrderBy(a => a.FirstSequenceStart));
            StatusEventEnd(Properties.Resources.ConsensusGenerationEnded);

            if (ScaffoldingEnabled)
            {
                // 5) Scaffold Generation
                StatusEventStart(Properties.Resources.ScaffoldGenerationStarted);
                IEnumerable<ISequence> scaffolds = ScaffoldsGenerator(contigs, reads);
                StatusEventEnd(Properties.Resources.ScaffoldGenerationEnded);

                return scaffolds;
            }
            else
            {
                return contigs;
            }
        }

        #region Assembler Steps
        /// <summary>
        /// Build scaffolds from contigs and paired reads (uses Padena Step 6 for assembly).
        /// </summary>
        /// <param name="contigs">List of contigs.</param>
        /// <param name="reads">List of paired reads.</param>
        /// <returns>List of scaffold sequences.</returns>
        private IEnumerable<ISequence> ScaffoldsGenerator(IEnumerable<ISequence> contigs, IEnumerable<ISequence> reads)
        {
            using (GraphScaffoldBuilder scaffoldBuilder = new GraphScaffoldBuilder())
            {
                return scaffoldBuilder.BuildScaffold(reads, contigs.ToList(), this.KmerLength, this.Depth, this.ScaffoldRedundancy);
            }
        }

        /// <summary>
        /// Generates a consensus sequence for the genomic region covered by reads.
        /// </summary>
        /// <param name="alignmentBetweenReferenceAndReads">Alignment between reference genome and reads.</param>
        /// <returns>List of contigs.</returns>
        private IEnumerable<ISequence> ConsensusGenerator(IEnumerable<DeltaAlignment> alignmentBetweenReferenceAndReads)
        {
            return ConsensusGeneration.GenerateConsensus(alignmentBetweenReferenceAndReads);
        }

        /// <summary>
        /// Refines layout of alignment between reads and reference genome by taking care of indels and rearrangements.
        /// </summary>
        /// <param name="orderedRepeatResolvedDeltas">Ordered Repeat Resolved Deltas.</param>
        private static void LayoutRefinment(IList<DeltaAlignment> orderedRepeatResolvedDeltas)
        {
            LayoutRefiner.RefineLayout(orderedRepeatResolvedDeltas);
        }

        /// <summary>
        /// Reads ambiguously placed due to genomic reads.
        /// This step requires mate pair information to resolve the ambiguity about placements of repeated sequences.
        /// </summary>
        /// <param name="alignmentBetweenReferenceAndReads">Alignment between reference genome and reads.</param>
        /// <returns>List of DeltaAlignments after resolving repeating reads.</returns>
        private static IEnumerable<DeltaAlignment> RepeatResolution(IEnumerable<IEnumerable<DeltaAlignment>> alignmentBetweenReferenceAndReads)
        {
            return RepeatResolver.ResolveAmbiguity(alignmentBetweenReferenceAndReads);
        }

        /// <summary>
        /// Aligns reads to reference genome using NUCmer.
        /// </summary>
        /// <param name="referenceSequence">Sequence of reference genome.</param>
        /// <param name="reads">List of sequence reads.</param>
        /// <returns>Delta alignments after read alignment.</returns>
        private IList<IEnumerable<DeltaAlignment>> ReadAlignment(IEnumerable<ISequence> referenceSequence, IEnumerable<ISequence> reads)
        {
            List<IEnumerable<DeltaAlignment>> deltaAlignments = new List<IEnumerable<DeltaAlignment>>();

            Parallel.ForEach(referenceSequence, sequence =>
            {
                NUCmer nucmer = new NUCmer((Sequence)sequence);

                // TODO: Update these hardcoded values with the once used in AMOSS while validating functionality with real data.
                nucmer.FixedSeparation = 0;
                nucmer.MinimumScore = 1;
                nucmer.SeparationFactor = 0;
                nucmer.MaximumSeparation = 0;
                nucmer.BreakLength = BreakLength;
                nucmer.LengthOfMUM = LengthOfMum;

                foreach (ISequence qrySequence in reads)
                {
                    deltaAlignments.Add(nucmer.GetDeltaAlignments(qrySequence, false));
                }
            });

            return deltaAlignments;
        }

        /// <summary>
        /// Raises status event.
        /// </summary>
        private void RaiseStatusEvent()
        {
            if (this.StatusChanged != null)
            {
                this.StatusChanged.Invoke(this, new StatusChangedEventArgs(this.statusMessage));
            }
        }

        /// <summary>
        /// Method to handle ProgressTimer elapsed event.
        /// </summary>
        /// <param name="sender">Progress timer.</param>
        /// <param name="e">Event arguments.</param>
        private void ProgressTimerElapsed(object sender, ElapsedEventArgs e)
        {
            this.statusMessage = Properties.Resources.DefaultSubStatus;
                    this.RaiseStatusEvent();
        }

        /// <summary>
        /// Raises status changed event with Step started status message.
        /// </summary>
        private void StatusEventStart(string message)
        {
            this.statusMessage = string.Format(CultureInfo.CurrentCulture, message, DateTime.Now);
            this.RaiseStatusEvent();
            this.progressTimer.Start();
        }

        /// <summary>
        /// Raises status changed event with Step ended status message.
        /// </summary>
        private void StatusEventEnd(string message)
        {
            this.progressTimer.Stop();
            this.statusMessage = string.Format(CultureInfo.CurrentCulture, message, DateTime.Now);
            this.RaiseStatusEvent();
        }

        #endregion
    }
}
